package cn.sciento.starter.lock;

import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;

import cn.sciento.starter.lock.annotation.Lock;
import cn.sciento.starter.lock.annotation.LockKey;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.context.expression.MethodBasedEvaluationContext;
import org.springframework.core.DefaultParameterNameDiscoverer;
import org.springframework.core.ParameterNameDiscoverer;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import org.springframework.util.StringUtils;

public class LockInfoProvider {
  private ParameterNameDiscoverer nameDiscoverer = (ParameterNameDiscoverer)new DefaultParameterNameDiscoverer();
  
  private ExpressionParser parser = new SpelExpressionParser();
  
  public LockInfo getLockInfo(JoinPoint joinPoint, Lock lock) {
    MethodSignature signature = (MethodSignature)joinPoint.getSignature();
    List<String> keyList = getKeyList(joinPoint, lock);
    String lockName = getLockName(lock.name(), signature, keyList);
    long waitTime = getLockWaitTime(lock);
    long leaseTime = getLockLeaseTime(lock);
    TimeUnit timeUnit = getLockTimeUnit(lock);
    return new LockInfo(lockName, keyList, waitTime, leaseTime, timeUnit);
  }
  
  private List<String> getKeyList(JoinPoint joinPoint, Lock lock) {
    Method method = getMethod(joinPoint);
    List<String> definitionKeys = getSpelDefinitionKey(lock.keys(), method, joinPoint.getArgs());
    List<String> keyList = new ArrayList<>(definitionKeys);
    List<String> parameterKeys = getParameterKey(method.getParameters(), joinPoint.getArgs());
    keyList.addAll(parameterKeys);
    return keyList;
  }
  
  private Method getMethod(JoinPoint joinPoint) {
    MethodSignature signature = (MethodSignature)joinPoint.getSignature();
    Method method = signature.getMethod();
    if (method.getDeclaringClass().isInterface())
      try {
        method = joinPoint.getTarget().getClass().getDeclaredMethod(signature.getName(), method
            .getParameterTypes());
      } catch (Exception e) {
        e.printStackTrace();
      }  
    return method;
  }
  
  private List<String> getSpelDefinitionKey(String[] definitionKeys, Method method, Object[] parameterValues) {
    List<String> definitionKeyList = new ArrayList<>();
    for (String definitionKey : definitionKeys) {
      if (definitionKey != null && !definitionKey.isEmpty()) {
        MethodBasedEvaluationContext methodBasedEvaluationContext = new MethodBasedEvaluationContext(null, method, parameterValues, this.nameDiscoverer);
        Object value = this.parser.parseExpression(definitionKey).getValue((EvaluationContext)methodBasedEvaluationContext);
        if (value != null)
          definitionKeyList.add(value.toString()); 
      } 
    } 
    return definitionKeyList;
  }
  
  private List<String> getParameterKey(Parameter[] parameters, Object[] parameterValues) {
    List<String> parameterKey = new ArrayList<>();
    for (int i = 0; i < parameters.length; i++) {
      if (parameters[i].getAnnotation(LockKey.class) != null) {
        LockKey keyAnnotation = parameters[i].<LockKey>getAnnotation(LockKey.class);
        if (keyAnnotation.value().isEmpty()) {
          parameterKey.add(parameterValues[i].toString());
        } else {
          StandardEvaluationContext context = new StandardEvaluationContext(parameterValues[i]);
          Object value = this.parser.parseExpression(keyAnnotation.value()).getValue((EvaluationContext)context);
          if (value != null)
            parameterKey.add(value.toString()); 
        } 
      } 
    } 
    return parameterKey;
  }
  
  private String getLockName(String annotationName, MethodSignature signature, List<String> keyList) {
    if (annotationName.isEmpty())
      return String.format("%s.%s.%s", new Object[] { signature.getDeclaringTypeName(), signature.getMethod().getName(), 
            StringUtils.collectionToDelimitedString(keyList, "", "-", "") }); 
    return annotationName;
  }
  
  private long getLockWaitTime(Lock lock) {
    return lock.waitTime();
  }
  
  private long getLockLeaseTime(Lock lock) {
    return lock.leaseTime();
  }
  
  private TimeUnit getLockTimeUnit(Lock lock) {
    return lock.timeUnit();
  }
}
